# Tagwire - A plugin for listing and handling "tags"
#
# $Id: tagwire.pl 211 2006-05-28 01:08:06Z ogawa $
#
# This software is provided as-is. You may use it for commercial or 
# personal use. If you distribute it, please keep this notice intact.
#
# Copyright (c) 2005,2006 Hirotaka Ogawa

package MT::Plugin::Tagwire;
use strict;
use MT;
use base qw(MT::Plugin);

use MT::Template::Context;
use MT::Entry;
use MT::Request;

# DEBUG
my $FORCE_PD_REFRESH = 0;

our $HAVE_MT_PLUGINDATA = 0;
our $HAVE_MT_XSEARCH = 0;

our $cache;
my $plugin;

BEGIN {
    our $VERSION = '0.26';
    $plugin = __PACKAGE__->new({
	name => 'Tagwire Plugin',
	description => 'A plugin for listing and handling blog-wide tags and entry tags.',
	doc_link => 'http://as-is.net/hacks/2005/06/tagwire_plugin.html',
	author_name => 'Hirotaka Ogawa',
	author_link => 'http://profile.typekey.com/ogawa/',
	version => $VERSION,
    });
    MT->add_plugin($plugin);

    eval { require MT::PluginData; $HAVE_MT_PLUGINDATA = 1 };

    if ($HAVE_MT_PLUGINDATA) {
	MT::Entry->add_callback('post_save', 10, $plugin, \&post_save);
	MT::Entry->add_callback('post_remove', 10, $plugin, \&post_remove);
    }

    $cache = MT::Request->instance;

    MT::Template::Context->add_container_tag(Tags => \&tags);
    MT::Template::Context->add_container_tag(EntryTags => \&entry_tags);
    MT::Template::Context->add_container_tag(RelatedTags => \&related_tags);
    MT::Template::Context->add_tag(Tag => \&tag);
    MT::Template::Context->add_tag(TagCount => \&tag_count);
    MT::Template::Context->add_tag(TagDate => \&tag_date);
    MT::Template::Context->add_tag(TagsTotal => \&tags_total);
    MT::Template::Context->add_tag(TagsTotalSum => \&tags_total_sum);
    MT::Template::Context->add_container_tag(EntriesWithTags => \&entries);
    MT::Template::Context->add_container_tag(MostRelatedEntries => \&most_related_entries);
    MT::Template::Context->add_global_filter(encode_urlplus => \&encode_urlplus);

    # For compatibility (this plugin was formerly named 'AllKeywords')
    MT::Template::Context->add_container_tag(AllKeywords => \&tags);
    MT::Template::Context->add_container_tag(EntryAllKeywords => \&entry_tags);
    MT::Template::Context->add_tag(AllKeyword => \&tag);
    MT::Template::Context->add_tag(AllKeywordCount => \&tag_count);
    MT::Template::Context->add_tag(AllKeywordsTotal => \&tags_total);
    MT::Template::Context->add_tag(AllKeywordsTotalSum => \&tags_total_sum);
    MT::Template::Context->add_container_tag(EntriesWithKeywords => \&entries);

    eval { require MT::XSearch; $HAVE_MT_XSEARCH = 1 };

    if ($HAVE_MT_XSEARCH) {
	MT::XSearch->add_search_plugin('Tagwire', {
	    label => 'Tag(Keyword) Search',
	    description => 'Tag(Keyword) Search plugin for MT-XSearch',
	    on_execute => \&xsearch_on_execute,
	    on_stash => \&xsearch_on_stash,
	});
	MT::Template::Context->add_container_tag(XSearchTags => \&xsearch_tags);
    }
}

sub post_save {
    my ($eh, $dummy, $obj) = @_;
    update_plugindata($obj);
}

sub post_remove {
    my ($eh, $obj) = @_;
    # set it to be "draft" to remove
    $obj->status(MT::Entry::HOLD()) if $obj->status == MT::Entry::RELEASE();
    update_plugindata($obj);
}

sub update_plugindata {
    return unless $HAVE_MT_PLUGINDATA && $plugin;
    my ($entry) = @_;
    my $refresh = $FORCE_PD_REFRESH || 0;
    my $version = $plugin->version;
    my $blog_id = $entry->blog_id;
    my $pd = MT::PluginData->load({ plugin => $plugin->name,
				    key => $blog_id });
    if (!$pd) {
	$pd = new MT::PluginData();
	$pd->plugin($plugin->name);
	$pd->key($blog_id);
	$refresh = 1;
    }
    my $data = $pd->data() || {};

    $refresh = 1 if !exists $data->{version} || ${$data->{version}} ne $version;

    my (%eindex, %tindex);
    if ($refresh) {
	my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
					  status => MT::Entry::RELEASE() });
	while (my $e = $iter->()) {
	    my @tags = split_tags($e->keywords, 1) or next;
	    $eindex{$e->id} = { tags => \@tags,
				created_on => $e->created_on };
	}
    } else {
	my $eid = $entry->id;
	%eindex = %{$data->{eindex}};
	delete $eindex{$eid} if exists $eindex{$eid};
	if ($entry->status == MT::Entry::RELEASE()) {
	    my @tags = split_tags($entry->keywords, 1);
	    $eindex{$eid} = { tags => \@tags,
			      created_on => $entry->created_on };
	}
    }
    foreach my $eid (keys %eindex) {
	my $ts = $eindex{$eid}->{created_on};
	foreach (@{$eindex{$eid}->{tags}}) {
	    push @{$tindex{$_}->{eids}}, $eid;
	    $tindex{$_}->{ts} = $ts if !exists $tindex{$_}->{ts} || $tindex{$_}->{ts} < $ts;
	}
    }
    $data->{version} = \$version;
    $data->{eindex} = \%eindex;
    $data->{tindex} = \%tindex;
    $pd->data($data);
    $pd->save or die $pd->errstr;
    $cache->cache('Tagwire::Cache::' . $blog_id, undef);
}

sub split_args {
    my ($string, $delimiter, $case_sensitive) = @_;
    return unless $string;
    $string =~ s/\#.*$//g;
    $string =~ s/(^\s+|\s+$)//g;
    $string = lc $string unless $case_sensitive;

    return split(/\s+/, $string) unless $delimiter;

    my @tags;
    foreach my $tag (split($delimiter, $string)) {
	$tag =~ s/(^\s+|\s+$)//g;
	push @tags, $tag if $tag;
    }
    @tags;
}

sub split_tags {
    my ($string, $case_sensitive) = @_;
    return unless $string;
    my @tags;
    $string =~ s/\#.*$//g;
    $string =~ s/(^\s+|\s+$)//g;
    $string = lc $string unless $case_sensitive;
#    $string =~ s/\[[^[]+\]//g; # uncomment this to discard [short title]

    if ($string =~ m/[;,|]/) {
	# tags separated by non-whitespaces
	while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^;,|]+)/g) {
	    my $tag = $1;
	    $tag =~ s/(^[\["'\s;,|]+|[\]"'\s;,|]+$)//g;
	    push @tags, $tag if $tag;
	}
    } else {
	# tags separated by whitespaces
	while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^\s]+)/g) {
	    my $tag = $1;
	    $tag =~ s/(^[\["'\s]+|[\]"'\s]+$)//g;
	    push @tags, $tag if $tag;
	}
    }
    @tags;
}

sub get_indexes {
    my $blog_id = shift or return;
    my $cname = 'Tagwire::Cache::' . $blog_id;
    return $cache->cache($cname) if defined $cache->cache($cname);
    $cache->cache($cname, undef);

    my $data;
    if ($HAVE_MT_PLUGINDATA && $plugin) {
	my $pd = MT::PluginData->load({ plugin => $plugin->name,
					key => $blog_id });
	$data = $pd->data() if $pd;

	$data = undef if (!exists $data->{version} || ${$data->{version}} ne $plugin->version);
    }

    $data = get_indexes_from_db($blog_id) unless defined $data;

    $cache->cache($cname, $data) if $data;
}

sub get_indexes_from_db {
    my $blog_id = shift or return;
    my ($eindex, $tindex);
    my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
				      status => MT::Entry::RELEASE() });
    while (my $e = $iter->()) {
	my @tags = split_tags($e->keywords, 1) or next;
	$eindex->{$e->id} = { tags => \@tags,
			      created_on => $e->created_on };
    }
    foreach my $eid (keys %$eindex) {
	my $eidx = $eindex->{$eid};
	my $ts = $eidx->{created_on};
	foreach (@{$eidx->{tags}}) {
	    push @{$tindex->{$_}->{eids}}, $eid;
	    $tindex->{$_}->{ts} = $ts if !exists $tindex->{$_}->{ts} || $tindex->{$_}->{ts} < $ts;
	}
    }

    my $data = { eindex => $eindex, tindex => $tindex };
    $data;
}

sub tags {
    my ($ctx, $args, $cond) = @_;

    # sort_by (tag/tag-case/count, default = tag)
    my $sort_by = $args->{sort_by} || 'tag';
    # sort_order (ascend/descend, default = ascend)
    my $sort_order = $args->{sort_order} || 'ascend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my $data = get_indexes($ctx->stash('blog_id')) or return '';
    my $tindex = $data->{tindex};

    my %tags;
    if ($case_sensitive) {
	foreach (keys %$tindex) {
	    my $tidx = $tindex->{$_};
	    $tags{$_} = [ scalar @{$tidx->{eids}}, $tidx->{ts} ];
	}
    } else {
	foreach (keys %$tindex) {
	    my $tidx = $tindex->{$_};
	    my $t = lc $_;
	    if (!exists $tags{$t}) {
		$tags{$t} = [ scalar @{$tidx->{eids}}, $tidx->{ts} ];
	    } else {
		$tags{$t}[0] += scalar @{$tidx->{eids}};
		$tags{$t}[1] = $tidx->{ts} if $tags{$t}[1] < $tidx->{ts};
	    }
	}
    }

    my @list;
    if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
	@list = $sort_order eq 'ascend' ?
	    sort { lc $a cmp lc $b } keys %tags :
	    sort { lc $b cmp lc $a } keys %tags;
    } elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
	@list = $sort_order eq 'ascend' ?
	    sort keys %tags :
	    sort reverse keys %tags;
    } else {
	@list = $sort_order eq 'ascend' ?
	    sort { $tags{$a}[0] <=> $tags{$b}[0] } keys %tags :
	    sort { $tags{$b}[0] <=> $tags{$a}[0] } keys %tags;
    }

    $ctx->stash('Tagwire::tags_total', scalar @list);

    my $total_sum = 0;
    $total_sum += $tags{$_}[0] foreach (@list);
    $ctx->stash('Tagwire::tags_total_sum', $total_sum);

    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    my $i = 0;
    foreach (@list) {
	last if $lastn && $i >= $lastn;
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	local $ctx->{__stash}{'Tagwire::tag_count'} = $tags{$_}[0];
	local $ctx->{__stash}{'Tagwire::tag_date'} = $tags{$_}[1];
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
	$i++;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub entry_tags {
    my ($ctx, $args, $cond) = @_;
    my $e = $ctx->stash('entry')
	or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
    return '' unless $e->keywords;

    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my @tags = split_tags($e->keywords, $case_sensitive);
    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    foreach (@tags) {
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub related_tags {
    my ($ctx, $args, $cond) = @_;
    my $tag = $ctx->stash('Tagwire::tag') or return '';

    my $sort_by = $args->{sort_by} || 'tag';
    # sort_order (ascend/descend, default = ascend)
    my $sort_order = $args->{sort_order} || 'ascend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my $data = get_indexes($ctx->stash('blog_id')) or return '';
    my ($tindex, $eindex) = ($data->{tindex}, $data->{eindex});

    my %tags;
    if ($case_sensitive) {
	foreach my $eid (@{$tindex->{$tag}->{eids}}) {
	    foreach (@{$eindex->{$eid}->{tags}}) {
		next if $_ eq $tag;
		if (!exists $tags{$_}) {
		    $tags{$_} = [ 1, $tindex->{$_}->{ts} ];
		} else {
		    $tags{$_}[0]++;
		}
	    }
	}
    } else {
	$tag = lc $tag;
	foreach my $nctag (grep { lc $_ eq $tag } keys %$tindex) {
	    foreach my $eid (@{$tindex->{$nctag}->{eids}}) {
		foreach (@{$eindex->{$eid}->{tags}}) {
		    my $t = lc $_;
		    next if $t eq $tag;
		    if (!exists $tags{$t}) {
			$tags{$t} = [ 1, $tindex->{$_}->{ts} ];
		    } else {
			$tags{$t}[0]++;
			$tags{$t}[1] = $tindex->{$_}->{ts} if $tags{$t}[1] < $tindex->{$_}->{ts};
		    }
		}
	    }
	}
    }
    my @list;
    if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
	@list = $sort_order eq 'ascend' ?
	    sort { lc $a cmp lc $b } keys %tags :
	    sort { lc $b cmp lc $a } keys %tags;
    } elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
	@list = $sort_order eq 'ascend' ?
	    sort keys %tags :
	    sort reverse keys %tags;
    } else {
	@list = $sort_order eq 'ascend' ?
	    sort { $tags{$a}[0] <=> $tags{$b}[0] } keys %tags :
	    sort { $tags{$b}[0] <=> $tags{$a}[0] } keys %tags;
    }

    $ctx->stash('Tagwire::tags_total', scalar @list);

    my $total_sum = 0;
    $total_sum += $tags{$_}[0] foreach (@list);
    $ctx->stash('Tagwire::tags_total_sum', $total_sum);

    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    my $i = 0;
    foreach (@list) {
	last if $lastn && $i >= $lastn;
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	local $ctx->{__stash}{'Tagwire::tag_count'} = $tags{$_}[0];
	local $ctx->{__stash}{'Tagwire::tag_date'} = $tags{$_}[1];
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
	$i++;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub tag {
    $_[0]->stash('Tagwire::tag') || '';
}

sub tag_count {
    $_[0]->stash('Tagwire::tag_count') || 0;
}

sub tag_date {
    my ($ctx, $args) = @_;
    $args->{ts} = $ctx->stash('Tagwire::tag_date')
	or return '';
    MT::Template::Context::_hdlr_date($ctx, $args);
}

sub tags_total {
    $_[0]->stash('Tagwire::tags_total') || 0;
}

sub tags_total_sum {
    $_[0]->stash('Tagwire::tags_total_sum') || 0;
}

sub entries {
    my ($ctx, $args, $cond) = @_;

    # tags(keywords) (REQUIRED)
    my $search = $args->{tags} || $args->{keywords} or return '';
    # delimiter for "tags" argument (default = space)
    my $delimiter = $args->{delimiter} || '';
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;
    # sort_by (created_on, default = created_on)
    my $sort_by = $args->{sort_by} || 'created_on';
    # sort_order (ascend/descend, default = descend)
    my $sort_order = $args->{sort_order} || 'descend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;

    my @tags = split_args($search, $delimiter, $case_sensitive)
	or return '';

    my $data = get_indexes($ctx->stash('blog_id')) or return '';
    my ($tindex, $eindex) = ($data->{tindex}, $data->{eindex});

    my %match;
    if ($case_sensitive) {
	foreach my $tag (@tags) {
	    foreach (@{$tindex->{$tag}->{eids}}) {
		$match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
	    }
	}
    } else {
	foreach my $tag (@tags) {
	    foreach my $t (grep { lc $_ eq $tag } keys %$tindex) {
		foreach (@{$tindex->{$t}->{eids}}) {
		    $match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
		}
	    }
	}
    }
    my $count = scalar @tags;
    my @eids = grep { $match{$_} == $count } keys %match or return '';
    @eids = $sort_order eq 'descend' ?
	sort { $eindex->{$b}->{created_on} <=> $eindex->{$a}->{created_on} } @eids :
	sort { $eindex->{$a}->{created_on} <=> $eindex->{$b}->{created_on} } @eids;
    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);

    my @entries;
    map { push @entries, MT::Entry->load($_) } @eids;

    my $res = '';
    my $tokens = $ctx->stash('tokens');
    my $builder = $ctx->stash('builder');
    my $i = 0;
    for my $e (@entries) {
	local $ctx->{__stash}{entry} = $e;
	local $ctx->{current_timestamp} = $e->created_on;
	local $ctx->{modification_timestamp} = $e->modified_on;
	my $out = $builder->build($ctx, $tokens, {
	    %$cond,
	    EntryIfExtended => $e->text_more ? 1 : 0,
	    EntryIfAllowComments => $e->allow_comments,
	    EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
	    EntryIfAllowPings => $e->allow_pings,
	    EntriesHeader => !$i,
	    EntriesFooter => !defined $entries[$i+1]
	    });
	return $ctx->error($ctx->errstr) unless defined $out;
	$res .= $out;
	$i++;
    }
    $res;
}

sub most_related_entries {
    my ($ctx, $args, $cond) = @_;
    my $entry = $ctx->stash('entry')
	or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
    return '' unless $entry->keywords;

    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;
    # sort_order (ascend/descend, default = descend)
    my $sort_order = $args->{sort_order} || 'descend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;

    my @tags = split_tags($entry->keywords, $case_sensitive)
	or return '';

    my $data = get_indexes($ctx->stash('blog_id')) or return '';
    my ($tindex, $eindex) = ($data->{tindex}, $data->{eindex});

    my %match;
    my $entry_id = $entry->id;
    if ($case_sensitive) {
	foreach my $tag (@tags) {
	    foreach (@{$tindex->{$tag}->{eids}}) {
		next if $_ == $entry_id;
		$match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
	    }
	}
    } else {
	foreach my $tag (@tags) {
	    foreach my $t (grep { lc $_ eq $tag } keys %$tindex) {
		foreach (@{$tindex->{$t}->{eids}}) {
		    next if $_ == $entry_id;
		    $match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
		}
	    }
	}
    }
    my @eids = keys %match or return '';
    @eids = $sort_order eq 'descend' ?
	sort { $eindex->{$b}->{created_on} <=> $eindex->{$a}->{created_on} } @eids :
	sort { $eindex->{$a}->{created_on} <=> $eindex->{$b}->{created_on} } @eids;
    @eids = sort { $match{$b} <=> $match{$a} } @eids;
    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
    my @entries;
    map { push @entries, MT::Entry->load($_) } @eids;

    my $res = '';
    my $tokens = $ctx->stash('tokens');
    my $builder = $ctx->stash('builder');
    my $i = 0;
    for my $e (@entries) {
	local $ctx->{__stash}{entry} = $e;
	local $ctx->{current_timestamp} = $e->created_on;
	local $ctx->{modification_timestamp} = $e->modified_on;
	my $out = $builder->build($ctx, $tokens, {
	    %$cond,
	    EntryIfExtended => $e->text_more ? 1 : 0,
	    EntryIfAllowComments => $e->allow_comments,
	    EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
	    EntryIfAllowPings => $e->allow_pings,
	    EntriesHeader => !$i,
	    EntriesFooter => !defined $entries[$i+1]
	    });
	return $ctx->error($ctx->errstr) unless defined $out;
	$res .= $out;
	$i++;
    }
    $res;
}

use MT::Util;
sub encode_urlplus {
    my $s = $_[0];
    return $s unless $_[1];
    $s =~ tr/ /+/;
    MT::Util::encode_url($s);
}

sub xsearch_tags {
    my ($ctx, $args, $cond) = @_;

    return '' unless defined $ctx->stash('xsearch_tags');
    my $tags = $ctx->stash('xsearch_tags');

    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    foreach (@$tags) {
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub xsearch_on_stash {
    my ($ctx, $val, $self) = @_;
    $ctx->stash('entry', $val);
    $ctx->{current_timestamp} = $val->created_on;
    $ctx->{modification_timestamp} = $val->modified_on;
    $ctx->stash('xsearch_tags', $self->{xsearch_tags});
}

sub xsearch_on_execute {
    my ($args, $self) = @_;

    MT->error('Blog ID is required.') unless $args->{blog_id};
    my $delimiter = $args->{delimiter} || '';
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;
    my $sort_by = $args->{sort_by} || 'created_on';
    my $sort_order = $args->{sort_order} || 'descend';
    my $lastn = $args->{lastn} || 0;

    my @tags = split_args($args->{search}, $delimiter, $case_sensitive)
	or return [];

    $self->{xsearch_tags} = \@tags;

    my $data = get_indexes($args->{blog_id}) or return [];
    my ($tindex, $eindex) = ($data->{tindex}, $data->{eindex});

    my %match;
    if ($case_sensitive) {
	foreach my $tag (@tags) {
	    foreach (@{$tindex->{$tag}->{eids}}) {
		$match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
	    }
	}
    } else {
	foreach my $tag (@tags) {
	    foreach my $t (grep { lc $_ eq $tag } keys %$tindex) {
		foreach (@{$tindex->{$t}->{eids}}) {
		    $match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
		}
	    }
	}
    }
    my $count = scalar @tags;
    my @eids = grep { $match{$_} == $count } keys %match or return [];
    @eids = $sort_order eq 'descend' ?
	sort { $eindex->{$b}->{created_on} <=> $eindex->{$a}->{created_on} } @eids :
	sort { $eindex->{$a}->{created_on} <=> $eindex->{$b}->{created_on} } @eids;
    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);

    my @entries;
    map { push @entries, MT::Entry->load($_) } @eids;

    \@entries;
}

1;
